package com.hmdp.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Component
@Slf4j
public class CacheClient {

    //设即redis封装工具类
    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    //普通key设置
    private void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }
    //热点key设置
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        //要设置逻辑过期时间，所以借用RedisData
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        //存入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }
    //缓存穿透工具类方法
    public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){
        //获取缓存
        String key = keyPrefix + id;
        String json=stringRedisTemplate.opsForValue().get(key);
        //存在则直接返回
        if (StrUtil.isNotBlank(json)) {
            //将其反序列化为指定类型bean返回
            return JSONUtil.toBean(json, type);
        }
        //判断是否是空值，即是否发生缓存穿透命中空值
        if(json!=null){
            return null;
        }
        //查询数据库，由于是工具类，不确定会传入何种参数，所以采用传入函数来编程,,apply表示执行该函数（只能接受1个参数并返回一个结果）
        R r = dbFallback.apply(id);
        if (json==null){
            //数据库也不存在则将空值写入redis，并设置过期时间，防止缓存穿透
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //数据库存在则写入redis，并设置过期时间
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit);
        return r;
    }
    //缓存击穿工具类方法
    //创建一个线程池，为后面新建线程进行数据重建
    private  static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);
    //解决缓存击穿问题，采用逻辑过期方式解决
    public <R,ID>R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit) {
        //先尝试从redis中获取店铺缓存
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //存在则直接返回
        if (StrUtil.isBlank(json)) {
            //由于预热过热点数据，所以其一定是存在的。
            return null;
        }
        //缓存命中后判断缓存是否过期
        //如果过期还会返回过期信息，所以下面判断先做未过期处理
        //先将json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject rjson=(JSONObject) redisData.getData();
        R r = JSONUtil.toBean(rjson, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //如果未过期直接返回信息
        if(expireTime.isAfter(LocalDateTime.now())) {
            return r;
        }
        //如果过期，需要缓存重建
        //1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        //2.判断是否获得锁
        if(isLock){
            //获得锁后须再次进行检查redis是否已更新为新数据即是否过期，如果未过期则无需重建
            //再次获取缓存
            String shopJson2 = stringRedisTemplate.opsForValue().get(key);
            //判断缓存是否过期
            RedisData redisData2 = JSONUtil.toBean(shopJson2, RedisData.class);
            LocalDateTime expireTime2 = redisData2.getExpireTime();
            if(expireTime2.isAfter(LocalDateTime.now())){
                return JSONUtil.toBean(shopJson2, type);
            }
            //3.获得锁则执行查询数据库操作，并将结果缓存到redis中，并返回
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //执行数据库查询操作
                    R r1 = dbFallback.apply(id);
                    //结果放入redis
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //返回过期的信息
        return r;
    }
    //获取锁
    public boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(flag);
    }
    //释放锁
    public void unlock(String key){
        stringRedisTemplate.delete(key);
    }
}
